package com.kalle.workflow.service.impl;

import com.greenpineyu.fel.FelEngine;
import com.greenpineyu.fel.FelEngineImpl;
import com.greenpineyu.fel.context.FelContext;
import com.kalle.workflow.elements.Process;
import com.kalle.workflow.elements.*;
import com.kalle.workflow.enums.FlowStatus;
import com.kalle.workflow.enums.ProcessStatus;
import com.kalle.workflow.enums.TaskType;
import com.kalle.workflow.mapper.*;
import com.kalle.workflow.query.FlowQuery;
import com.kalle.workflow.service.FlowService;
import com.kalle.workflow.util.IdGenerator;
import com.kalle.workflow.util.UUIDGenerator;
import org.apache.ibatis.session.SqlSession;

import java.util.*;

public class FlowServiceImpl implements FlowService {

    private final SqlSession sqlSession;
    private final IdGenerator idGenerator;

    private final FlowMapper flowMapper;
    private final TaskMapper taskMapper;
    private final SequenceFlowMapper sequenceFlowMapper;
    private final ProcessMapper processMapper;
    private final VariableMapper variableMapper;

    public FlowServiceImpl(SqlSession sqlSession, IdGenerator idGenerator) {
        this.sqlSession = sqlSession;
        this.flowMapper = sqlSession.getMapper(FlowMapper.class);
        this.taskMapper = sqlSession.getMapper(TaskMapper.class);
        this.sequenceFlowMapper = sqlSession.getMapper(SequenceFlowMapper.class);
        this.processMapper = sqlSession.getMapper(ProcessMapper.class);
        this.variableMapper = sqlSession.getMapper(VariableMapper.class);
        this.idGenerator = Optional.ofNullable(idGenerator).orElse(UUIDGenerator.INSTANCE);
    }

    @Override
    public FlowQuery createQuery() {
        return new FlowQuery(flowMapper);
    }

    @Override
    public Flow complete(Flow flow) {
        return complete(flow, null);
    }

    @Override
    public Flow complete(Flow flow, Map<String, Object> params) {
        Flow flowInDB = flowMapper.selectById(flow.getId());
        if (flowInDB == null) {
            throw new RuntimeException("Flow not found in database.");
        } else if (flowInDB.getFlowStatus() == FlowStatus.FINISHED) {
            throw new RuntimeException("Flow has already been finished and can not be completed again.");
        }
        List<SequenceFlow> sequenceFlows = sequenceFlowMapper.selectBySourceRefId(flow.getTaskId());
        if (sequenceFlows.isEmpty()) {
            throw new RuntimeException("No target task with flowId: " + flow.getId());
        } else if (sequenceFlows.size() > 1) {
            throw new RuntimeException("More than one target task found with flowId: " + flow.getId());
        }
        //find next task id
        String nextTaskId = sequenceFlows.get(0).getTargetRefId();
        Task nextTask = taskMapper.selectById(nextTaskId);
        //fix exclusive gateway bug
        //check next task type is exclusive gateway ?
        while (nextTask.getTaskType() == TaskType.EXCLUSIVE_GATEWAY) {
            //reset sequenceFlows when task is exclusive gateway
            sequenceFlows = sequenceFlowMapper.selectBySourceRefId(nextTask.getId());
            if (null != params && !params.isEmpty()) {
                Iterator<SequenceFlow> iterator = sequenceFlows.iterator();
                while (iterator.hasNext()) {
                    SequenceFlow sequenceFlow = iterator.next();
                    String expression = sequenceFlow.getExpression();
                    if (expression == null) {
                        continue;
                    }
                    FelEngine felEngine = new FelEngineImpl();
                    FelContext context = felEngine.getContext();
                    for (Map.Entry<String, Object> entry : params.entrySet()) {
                        context.set(entry.getKey(), entry.getValue());
                    }
                    boolean match = (boolean) felEngine.eval(expression, context);
                    //if not match, remove this sequence flow
                    if (!match) {
                        iterator.remove();
                    }
                }
            }
            //check sequence flow
            if (sequenceFlows.isEmpty()) {
                throw new RuntimeException("No target task with flowId: " + flow.getId());
            } else if (sequenceFlows.size() > 1) {
                throw new RuntimeException("More than one target task found with flowId: " + flow.getId());
            }
            //find next task id
            nextTaskId = sequenceFlows.get(0).getTargetRefId();
            nextTask = taskMapper.selectById(nextTaskId);
        }
        //update current flow
        flow.setFlowStatus(FlowStatus.FINISHED);
        flow.setUpdateTime(new Date());
        flowMapper.update(flow);
        //insert variable
        if (params != null && !params.isEmpty()) {
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                Variable variable = new Variable();
                variable.setId(idGenerator.nextId());
                variable.setFlowId(flowInDB.getId());
                variable.setKey(entry.getKey());
                variable.setValue(entry.getValue().toString());
                variable.setJavaType(entry.getValue().getClass().getName());
                variableMapper.insert(variable);
            }
        }
        //create next flow
        Flow nextFlow = new Flow();
        nextFlow.setId(idGenerator.nextId());
        nextFlow.setDeploymentId(flow.getDeploymentId());
        nextFlow.setProcessId(flow.getProcessId());
        nextFlow.setTaskId(nextTaskId);
        //set assignee
        String assignee = nextTask.getAssignee();
        if (null != assignee && assignee.startsWith("${") && assignee.endsWith("}")) {
            if (null == params || params.isEmpty()) {
                throw new RuntimeException("Assignee parameter must not be null with expression: " + assignee);
            }
            String assigneeExpression = assignee.substring(assignee.indexOf("{" + 1), assignee.lastIndexOf("}"));
            FelEngine felEngine = new FelEngineImpl();
            FelContext context = felEngine.getContext();
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                context.set(entry.getKey(), entry.getValue());
            }
            assignee = (String) felEngine.eval(assigneeExpression, context);
        }
        nextFlow.setAssignee(assignee);
        nextFlow.setFlowStatus(FlowStatus.ACTIVE);
        nextFlow.setCreateTime(new Date());
        //if this is end event element, then update process status as FINISHED
        if (nextTask.getTaskType() == TaskType.END_EVENT) {
            nextFlow.setFlowStatus(FlowStatus.FINISHED);
            Process process = processMapper.selectById(nextFlow.getProcessId());
            process.setProcessStatus(ProcessStatus.FINISHED);
            process.setUpdateTime(new Date());
            processMapper.update(process);
        }
        flowMapper.insert(nextFlow);
        sqlSession.commit();
        return nextFlow;
    }

}
